require 'socket'
require 'thread'
require 'time'

class Symbol
  def to_s
    @_to_s || (@_to_s = id2name)
  end
end

# String extension methods.
class String
  # Encodes a normal string to a URI string.
  def uri_escape
    gsub(/([^ a-zA-Z0-9_.-]+)/n) {'%'+$1.unpack('H2'*$1.size).
      join('%').upcase}.tr(' ', '+')
  end
  
  # Decodes a URI string to a normal string.
  def uri_unescape
    tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){[$1.delete('%')].pack('H*')}
  end
  
  # Concatenates a path
  def /(o)
    to_s + '/' + o.to_s
  end
end

module ServerSide
  class Request
    module Const
      LineBreak = "\r\n".freeze
      RequestRegexp = /([A-Za-z]+)\s(\/.*)\sHTTP\/(.+)\r/.freeze
      HeaderRegexp = /([^:]+):\s?(.*)\r\n/.freeze
      ContentLength = 'Content-Length'.freeze
      Version_1_1 = '1.1'.freeze
      Connection = 'Connection'.freeze
      Close = 'close'.freeze
      QueryRegexp = /([^\?]+)(\?(.*))?/.freeze
    end
    
    def initialize(conn)
      @conn = conn
      @thread = Thread.new {process}
      @thread[:time] = Time.now
      @thread[:stage] = 'normal'
    end
    
    def process
      while true
        break unless parse_request
        respond
        break unless @persistent
      end
    rescue => e
      puts e.message
      puts e.backtrace.first
    ensure
      @conn.close
    end
    
    def parse_request
      return nil unless @conn.gets =~ Const::RequestRegexp
      @method, @query, @version = $1.downcase.to_sym, $2, $3
      @query =~ Const::QueryRegexp
      @path = $1
      @parameters = $3 ? parse_parameters($3) : {}
      @headers = {}
      while (line = @conn.gets)
        break if line.nil? || (line == Const::LineBreak)
        if line =~ Const::HeaderRegexp
          @headers[$1] = $2
        end
      end
      @persistent = (@version == Const::Version_1_1) && 
        (@headers[Const::Connection] != Const::Close)
      @headers
    end
    
    module Const
      Ampersand = '&'.freeze
      ParameterRegexp = /(.+)=(.*)/.freeze
      EqualSign = '='.freeze
    end
    
    def parse_parameters(query)
      query.split(Const::Ampersand).inject({}) do |m, i|
        if i =~ Const::ParameterRegexp
          m[$1.to_sym] = $2.uri_unescape
        end
        m
      end
    end
    
    module Const
      Status = "HTTP/1.1 %d\r\nDate: %s\r\nConnection: close\r\nContent-Type: 
%s\r\nContent-Length: %d\r\n%s\r\n".freeze
      StatusStream = "HTTP/1.1 %d\r\nDate: %s\r\nConnection: close\r\nContent-Type: 
%s\r\n%s\r\n".freeze
      StatusPersist = "HTTP/1.1 %d\r\nDate: %s\r\nContent-Type: %s\r\nContent-Length: 
%d\r\n%s\r\n".freeze
      
      Header = "%s: %s\r\n".freeze
      Empty = ''.freeze
    end
    
    def send_response(status, content_type, body = nil, content_length = nil, 
      headers = nil)
      h = headers ? 
        headers.inject("") {|m, kv| m << (Const::Header % kv)} : Const::Empty
      
      content_length = body.length if content_length.nil? && body
      @persistent = false if content_length.nil?
      date = Time.now.httpdate
      
      if @persistent
        @conn << (Const::StatusPersist % [status, date, content_type, 
content_length, h])
      elsif body
        @conn << (Const::Status % [status, date, content_type, content_length, h])
      else
        @conn << (Frozen::StatusStream % [status, date, content_type, h])
      end
      (@conn << body if body) rescue nil
    end
    
    module Const
      HTML = '<html><body><h1>%s %s</h1><p>%s</p></body></html>'.freeze
      TextHtml = 'text/html'.freeze
      ETag = 'ETag'.freeze
      ETagFormat = '%x:%x:%x'.inspect.freeze
      CacheControl = 'Cache-Control'.freeze
      StaticAge = (86400 * 30).freeze
      StaticMaxAge = "max-age=#{StaticAge}".freeze
      IfNoneMatch = 'If-None-Match'.freeze
      TextPlain = 'text/plain'.freeze
      NotModifiedStatus = "HTTP/1.1 304 Not Modified\r\nConnection: close\r\nContent-Length: 0\r\nETag: %s\r\nCache-Control: max-age=#{StaticAge}\r\n\r\n".freeze
      NotModifiedStatusPersist = "HTTP/1.1 304 Not Modified\r\nContent-Length: 0\r\nETag: %s\r\nCache-Control: max-age=#{StaticAge}\r\n\r\n".freeze
    end
    
    @@mime_types = Hash.new {|h, k| Const::TextPlain}
    
    @@mime_types.merge!({
      '.html'.freeze => 'text/html'.freeze,
      '.css'.freeze => 'text/css'.freeze,
      '.js'.freeze => 'text/javascript'.freeze,

      '.gif'.freeze => 'image/gif'.freeze,
      '.jpg'.freeze => 'image/jpeg'.freeze,
      '.jpeg'.freeze => 'image/jpeg'.freeze,
      '.png'.freeze => 'image/png'.freeze
    })
    
    module Const
      TestRegexp = /^\/test/.freeze
    end

    def respond
      if @path =~ Const::TestRegexp
        send_response(200, Const::TextHtml, 
          Const::HTML % [@method, @path, ENV.inspect])
      else
        serve_file(@path)
      end
    end
    
    @@static_files = {}
    
    def serve_file(path)
      fn = '.'/path
      if File.file?(fn)
        stat = File.stat(fn)
        etag = Const::ETagFormat % [stat.mtime.to_i, stat.size, stat.ino]
        unless etag == @headers[Const::IfNoneMatch]
          if @@static_files[fn] && (@@static_files[fn][0] == etag)
            content = @@static_files[fn][1]
          else
            content = IO.read(fn).freeze
            @@static_files[fn] = [etag.freeze, content]
          end
          send_response(200, @@mime_types[File.extname(fn)], content, stat.size, {
            Const::ETag => etag,
            Const::CacheControl => Const::StaticMaxAge
          })
        else
          if @persistent
            @conn << Const::NotModifiedStatusPersist % etag
          else
            @conn << Const::NotModifiedStatus % etag
          end
#          send_response(304, Const::Empty, Const::Empty, 0, {
#            Const::ETag => etag,
#            Const::CacheControl => Const::StaticMaxAge
#          })
        end
      else
        send_response(404, Const::TextHtml, 'File not found.')
      end
    rescue => e
      puts e.message
      puts e.backtrace.first
      send_response(404, Const::TextHtml, 'Error reading file.')
    end
  end
  
  class Server
    attr_accessor :thread
    
    def initialize(host, port, request_class)
      @request_class = request_class
      @server = TCPServer.new(host, port)
      loop {@request_class.new(@server.accept)}
    end
  end
end

trap ("TERM") {exit}
ServerSide::Server.new('0.0.0.0', 4401, ServerSide::Request)
